package main

/* 
by zy, 2025.11.02

封装一个可以在发送 HTTP 请求前计算七牛认证签名的 http.Client。

使用示例：
    client := NewQiNiuClient(cfg)      // 调用 NewQiNiuClient(*CLI) 返回封装后的 client
    resp, err := client.Do(req)        // 调用 client.Do(*http.Request) 发送请求

原理：
    自定义的 SigningTransport 实现了 Transport.RoundTrip()。
    它会先计算签名并将 Authorization 头添加到请求中，
    然后再调用 Go 默认的 Transport（http.DefaultTransport）完成 HTTP 请求的发送和处理。
*/

import (
  "io"
  "fmt"
  "bytes"
  "net/http"
  "crypto/hmac"
  "crypto/sha1"
  "encoding/base64"
)

type SigningTransport struct {
  Base http.RoundTripper // net/http/client.go
  Cfg *CLI // types.go
}

// 计算七牛云 CDN 认证签名,
// 签名失败返回 error, 成功会在 req.Header 增加 Authorization 字段并返回 nil
func qiniuSign(cfg *CLI, req *http.Request) error {
  signingStr := req.URL.EscapedPath() // <path>
  if req.URL.RawQuery != "" {
    signingStr += "?" + req.URL.RawQuery // <path>?<query>
  }
  signingStr += "\n" // "<path>[?<query]\n"

  //七牛云文档发中要求当 Content-Type 为 application/x-www-form-urlencoded 时，签名内容必须包括请求内容, 其它情况实测不包含<body>
  if req.Method == http.MethodPost && req.Body != nil && req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
    bodyBytes, err := io.ReadAll(req.Body)
    if err != nil {
      return fmt.Errorf("qiniuSign: %s?%s failed to read body while signing: %w", req.URL.Path, req.URL.RawQuery, err)
    }

    // io.ReadAll(req.Body) 会把 req.Body 消耗掉，需要重新恢复 req.Body
    req.Body = io.NopCloser(bytes.NewReader(bodyBytes))

    // 非空的 Body 加入待签名字符串
    if len(bodyBytes) > 0 {
      signingStr += string(bodyBytes)
    }
  }

  // 计算 HMAC-SHA1 签名
  mac := hmac.New(sha1.New, []byte(cfg.SecretKey))
  mac.Write([]byte(signingStr))
  sign := mac.Sum(nil)
  // URL-Safe 编码
  encodedSign := base64.URLEncoding.EncodeToString(sign) 
  // 设置 Authorization 头
  accessToken := fmt.Sprintf("QBox %s:%s", cfg.AccessKey, encodedSign)
  req.Header.Set("Authorization", accessToken)

  // 签名成功返回 nil
  return nil
}

func (t *SigningTransport) RoundTrip(req *http.Request) (*http.Response, error) {
  err := qiniuSign(t.Cfg, req)
  if err != nil {
    return nil, err
  }
  return t.Base.RoundTrip(req)
}

// 封装一个可以自动计算七牛签名的 http.Client
func NewQiNiuClient(cfg *CLI) *http.Client {
  return &http.Client{
    Transport: &SigningTransport{
      Base: http.DefaultTransport,
      Cfg: cfg,
    },
  }
}

